在昨天實做完結構化 logger 之後,接著今天將會來實做 graceful shutdown。
所謂的 graceful shutdown 目的在於,當伺服器關閉時需要能有一個妥善釋放資源的設計。比如,當接收到關機的訊號,程式把處理到一半的任務先處理完再關閉,通常會設定一個 Timeout ,限定在這個 Timeout 時間內讓伺服企釋放資源。讓原本的任務有機會寫入其他狀態,等下次重啟再繼續。
在 golang 可以透過 signal channel 來監聽伺服器關閉訊號,然後透過 context 來傳遞訊號到其他 goroutine 通知相關處理資源的 routine 在 timeout 時限內,做完資源釋放。
在 nodejs 這邊主要是透過 process 的事件監聽器來處理這件事情。而在 nestjs 可以透過設定 onModuleDestory 的監測當伺服器關閉的事件,來作邏輯處理。
nestjs 應用程式的生命週期事件,如下圖:
其中,綠色的方塊是能夠透過程式去編寫的部份。而要作到 graceful shutdown 。主要讓 app.enableShutdownHooks() 才能偵測到 terminal signal 的訊號。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { Logger, PinoLogger } from 'nestjs-pino';
import { Request } from 'express';
let app: INestApplication;
const pinoLogger = new PinoLogger({
pinoHttp: {
autoLogging: false,
base: null,
quietReqLogger: true,
genReqId: (request: Request) => request.headers['x-correlation-id'] || crypto.randomUUID(),
level: 'info',
formatters: {
level (label) {
return {level: label }
}
},
}
});
async function bootstrap() {
app = await NestFactory.create(AppModule, {
bufferLogs: true,
});
app.useLogger(app.get(Logger));
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
transform: true,
}));
// enable shutdown event listener
app.enableShutdownHooks();
await app.listen(3000);
}
bootstrap();
// setup graceful shutdown logic
const gracefulShutdown = (signal?: unknown) => {
pinoLogger.info('server gracefulShutdown...', signal);
app.close().then(() =>{
pinoLogger.info('server sucessfully shutdown', signal);
process.exit(0);
})
.catch((err: unknown) => pinoLogger.error('server shutdown failed', err));
setTimeout(() => {
pinoLogger.error('force to shutdown after 5 seconds');
process.exit(1);
}, 5000);
}
process.on('uncaughtException', gracefulShutdown);
process.on('unhandledRejection', gracefulShutdown);
適當的 graceful shutdown ,能夠正確的釋放伺服器佔有的資源。避免因為資源被本沒處理好的連線狀態干擾,這對系統狀態極為重要。特別是,如果沒有恰當的去處理,在後面將伺服器作容器化時,就容易看到 exit code 137 的狀態碼,讓資源無法正確的釋放。